问题:
1、为什么项目路径任意目录下都可以执行rails s,启动项目
2、rails s和rails server有什么区别,以及是如何实现的
3、终端是执行ctrl + c后,rails做了什么
4、是何时加载gem的
5、rails s 之后都做了哪些工作
1. gem ‘railties’ 是rails的核心gem, 处理rails程序的引导入口,管理rails的命令行接口
1.1 执行rails s 首先会执行 railties下的 bin/rails 可执行文件,内容如下
1 2 3 4 5 6 7 8 9
| git_path = File.expand_path('../../../.git', __FILE__) if File.exist?(git_path) railties_path = File.expand_path('../../lib', __FILE__) $:.unshift(railties_path) end require "rails/cli"
|
加载了 rails/cli,此文件加载了 rails/app_rails_loader,并执行exec_app_rails方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| def exec_app_rails original_cwd = Dir.pwd loop do if exe = find_executable contents = File.read(exe) if contents =~ /(APP|ENGINE)_PATH/ exec RUBY, exe, *ARGV break elsif exe.end_with?('bin/rails') && contents.include?('This file was generated by Bundler') $stderr.puts(BUNDLER_WARNING) Object.const_set(:APP_PATH, File.expand_path('config/application', Dir.pwd)) require File.expand_path('../boot', APP_PATH) require 'rails/commands' break end end Dir.chdir(original_cwd) and return if Pathname.new(Dir.pwd).root? Dir.chdir('..') end end def find_executable EXECUTABLES.find { |exe| File.file?(exe) } end end
|
此方法会找到项目路径下的,bin/rails文件,并执行
bin/rails文件如下
1 2 3 4
| APP_PATH = File.expand_path('../../config/application', __FILE__) require_relative '../config/boot' require 'rails/commands'
|
config/boot 会加载 bundler/setup,Bundler 通过它设置 Gemfile 中依赖关系的加载路径。
执行完config/boot文件后,继续加载 rails/commands
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| ARGV << '--help' if ARGV.empty? aliases = { "g" => "generate", "d" => "destroy", "c" => "console", "s" => "server", "db" => "dbconsole", "r" => "runner" } command = ARGV.shift command = aliases[command] || command require 'rails/commands/commands_tasks' Rails::CommandsTasks.new(ARGV).run_command!(command)
|
他的作用是扩展命令别名,如果输入rails s会在aliases查找到对应的命令,相当于执行 rails server
接着加载rails/commands/commands_tasks 并执行Rails::CommandsTasks.new(ARGV).run_command!(command)
1 2 3 4 5 6 7 8 9 10 11 12
| def initialize(argv) @argv = argv end def run_command!(command) command = parse_command(command) if COMMAND_WHITELIST.include?(command) send(command) else write_error_message(command) end end
|
接着通过send(command)方法会执行server方法,然后运行 Rails::Server.new.start
1 2 3 4 5 6 7 8 9 10 11 12
| def server set_application_directory! require_command!("server") Rails::Server.new.tap do |server| require APP_PATH Dir.chdir(Rails.application.root) server.start end end
|
rails/commands/server,Rails::Server 类继承自Rack::Server 类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| def initialize(*) super set_environment end def start print_boot_information trap(:INT) { exit } create_tmp_directories log_to_stdout if options[:log_stdout] super ensure puts 'Exiting' unless @options && options[:daemonize] end
|
当调用Rails::Server.new方法时,会调用Rack::Server类的initialize方法。config/application.rb 文件加载完成后,会调用 server.start 方法。
这个时候rails通过print_boot_information方法第一次输出信息,并为 INT 信号创建了一个回调,只要在服务器运行时按下 CTRL-C,服务器进程就会退出。用Signal.list可以查看支持的信号类型。
并且会创建tmp等文件。super 方法会调用 Rack::Server.start 方法。
lib/rack/server.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| def initialize(options = nil) @ignore_options = [] if options @use_default_options = false @options = options @app = options[:app] if options[:app] else argv = defined?(SPEC_ARGV) ? SPEC_ARGV : ARGV @use_default_options = true @options = parse_options(argv) end end def parse_options(args) args.clear if ENV.include?(REQUEST_METHOD) @options = opt_parser.parse!(args) @options[:config] = ::File.expand_path(options[:config]) ENV["RACK_ENV"] = options[:environment] @options end def opt_parser Options.new end def start &blk if options[:warn] $-w = true end if includes = options[:include] $LOAD_PATH.unshift(*includes) end if library = options[:require] require library end if options[:debug] $DEBUG = true require 'pp' p options[:server] pp wrapped_app pp app end check_pid! if options[:pid] wrapped_app daemonize_app if options[:daemonize] write_pid if options[:pid] trap(:INT) do if server.respond_to?(:shutdown) server.shutdown else exit end end server.run wrapped_app, options, &blk end
|
我们来看下wrapped_app
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| def wrapped_app @wrapped_app ||= build_app app end def app @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config end private def build_app_and_options_from_config if !::File.exist? options[:config] abort "configuration #{options[:config]} not found" end app, options = Rack::Builder.parse_file(self.options[:config], opt_parser) self.options.merge! options app end def build_app_from_string Rack::Builder.new_from_string(self.options[:builder]) end
|
options[:config] 的默认值为 config.ru,此文件
1 2
| require ::File.expand_path('../config/environment', __FILE__) run Rails.application
|
Rack::Builder.parse_file 方法读取 config.ru 文件的内容,并执行
1 2 3 4
| require ::File.expand_path('../config/environment', __FILE__) run Rails.application
|
config/environment.rb
1 2 3 4 5
| require File.expand_path('../application', __FILE__) Rails.application.initialize!
|
config/application.rb
1 2 3 4 5 6 7 8
| require File.expand_path('../boot', __FILE__) ENV['NLS_LANG'] = 'AMERICAN_AMERICA.UTF8' require 'rails/all' Bundler.require(*Rails.groups)
|
加载rails依赖,以及gem
railties/lib/rails/application.rb
1 2 3 4 5 6
| def initialize!(group=:default) raise "Application has been already initialized." if @initialized run_initializers(group, self) @initialized = true self end
|
railties/lib/rails/initializable.rb
1 2 3 4 5 6 7
| def run_initializers(group=:default, *args) return if instance_variable_defined?(:@ran) initializers.tsort_each do |initializer| initializer.run(*args) if initializer.belongs_to?(group) end @ran = true end
|
Rails 会遍历所有类的祖先,以查找能够响应 initializers 方法的类。对于找到的类,首先按名称排序,然后依次调用 initializers 方法。应用初始化完成后,程序执行流程再次回到 Rack::Server 类。
Rack:lib/rack/server.rb
1
| server.run wrapped_app, options, &blk
|
总结一下,看看rails s启动都加载了哪些类和执行了哪些方法
1. 执行railties/exe/rails 加载cli文件
2. cli文件加载rails/app_rails_loader执行Rails::AppRailsLoader.exec_app_rails方法来遍历查找项目路径下的bin/rails可执行程序,找到后执行
3. bin/rails文件加载config/boot.rb(设置gem的加载路径)文件,并加载rails/commands
4. rails/commands设置了命令的扩展别名,加载rails/commands/commands_tasks类,并执行Rails::CommandsTasks.new(ARGV).run_command!(command)。
5. commands/commands_tasks 判断命令是否为预设的命令,如果不存在,就输入错误信息,如果存在执行对应的方法 server,server加载config/application 并初始化Rails::Server类执行start方法。
6. rails/commands/server start方法,输出项目信息,并为INT信号创建陷阱,还会创建tmp等文件内容,super继承父类,继续执行继承自父类Rack::Server的start方法。
7. lib/rack/server.rb wrapped_app方法会加载config.ru(加载config/envirment(加载config/application(加载rails/all和gems)并初始化Rails.application.initialize!),并执行run Rails.application)返回app,server.run 方法的实现方式取决于我们所使用的服务器,如果使用的是puma实际执行的是Rack::Handler::Puma.run进入服务器端口监听循环。
参考文档: rails中文指南 rails s 启动过程分析